<?php

namespace system\core;

if( !defined('PHP_VERSION_ID') )
{
	$_v = explode('.', PHP_VERSION);
	define('PHP_VERSION_ID', ($_v[0] * 10000 + $_v[1] * 100 + $_v[2]));
}
if( PHP_VERSION_ID < 50200 )
{
	die(basename(__FILE__)." needs at least PHP 5.2 to work.");
}

if( !function_exists("_") )
{

	function _($d="")
	{
		$A = is_array($d);
		$S = isset($_SERVER['HTTP_USER_AGENT']);
		echo($A && $S ? '<pre>' : '').print_r($d, true).($A && $S ? '</pre>' : '').($S ? "<br>\n" : "\n");
	}

}

if( !function_exists("struct") )
{

	function struct($array=false, $key=false, $return=false)
	{
		return struct::draw($array, $key, $return);
	}

}

/**
 * Struct is a class to outsource and simplify the creation of arrays.
 * The syntax used to create structured data is similar to php except some unneeded markups.
 * Syntax example for a stuct key within php (string)
 * struct::set("layer1->layer2->layer3=A String");
 *
 * Syntax example for a stuct key within an external file
 * SET layer1->layer2->layer3=A String
 * @author echterhoff
 * @license LGPL see license.txt
 *
 */
class struct
{

	private $store = array();
	private $meta = array();
	private $assignoperator = "[:=]";
	private $accesskey = array();
	private $keyaccess = array();
	private $lastkey;
	private $response_debug;
	private $group;
	private $each = array();
	private $usecache = true;

	function __construct($usecache=true)
	{
		$this->usecache = $usecache;
	}

	/**
	 * Command to control the target group for a struct modification
	 * @param mixed $key
	 */
	public function group($key=false)
	{
		if( $key )
		{
			$this->group = $key."->";
		}
		else
		{
			unset($this->each[$this->group]);
			$this->group = "";
		}
	}

	/**
	 * Define any meta data to a key
	 * @param string $key
	 * @param string $metaname
	 * @param string $val
	 */
	public function set_meta($key, $metaname=null, $val=null)
	{
		return $this->def($key, $metaname, $val);
	}

	/**
	 * Short alias for set_meta
	 * @param string $key
	 * @param string $metaname
	 * @param string $val
	 */
	public function meta($key, $metaname=null, $val=null)
	{
		return $this->def($key, $metaname, $val);
	}

	/**
	 * Short alias for set_meta
	 * @param string $key
	 * @param string $metaname
	 * @param string $val
	 */
	public function def($key, $metaname=null, $val=null)
	{
		$autotrue = false;
		if( !isset($metaname) && !isset($val) )
		{
			$metaname = $key;
			$autotrue = true;
		}
		if( isset($val) )
		{
			$this->lastkey = $key;
		}
		if( !isset($val) )
		{
			$val = $metaname;
			$metaname = $key;
		}
		if( $autotrue )
		{
			$val = true;
		}
		//echo $metaname."\n";
		$this->evaluate_value($val);
		$this->set_array($this->meta, $this->group.$this->lastkey."->".$metaname, $val);
		return true;
	}

	private function evaluate_value(&$val)
	{
		$tmpVal = trim($val);
		if( $tmpVal[0] === "{" and $tmpVal[strlen($tmpVal) - 1] === "}" )
		{
			$tmpVal = substr($tmpVal, 1, -1);
			$tmpVal = preg_split("/[ ]*,[ ]*/", $tmpVal);
			if( $tmpVal[0] == "" )
			{
				$val = "";
				return;
			}
			$val = $tmpVal;
		}
	}

	/**
	 * Belongs to the response method and tries to record all events to $this->response_debug
	 * @param boolean $setto
	 */
	public function response_debug($setto=true)
	{
		ob_start();
		$this->response_debug = $setto;
	}

	/**
	 * Returns the desired element
	 * @param string $trigger
	 * @param string $key
	 * @param string $val
	 * @return array
	 */
	public function response($trigger="ajax", $key="key", $val="val")
	{
		if( isset($_REQUEST[$trigger]) )
		{
			if( isset($this->keyaccess[$_REQUEST[$key]]) )
			{
				$ret = array("key" => $this->keyaccess[$_REQUEST[$key]], "val" => $_REQUEST[$val]);
			}
			elseif( isset($this->accesskey[$_REQUEST[$key]]) )
			{
				$ret = array("key" => $_REQUEST[$key], "val" => $_REQUEST[$val]);
			}
			if( $this->response_debug )
			{
				ob_end_clean();
				echo json_encode($ret);
				exit;
			}
			return $ret;
			exit;
		}
		return false;
	}

	/**
	 * Helper to access method to retrieve meta data
	 * @param string $name
	 * @param mixed $args
	 * @return mixed
	 */
	public function __call($name, $args)
	{
		$meta = $this->get($args[0], $this->meta);
		return (!isset($meta[$name]) ? null : $meta[$name]);
	}

	/**
	 * Sets a value to the corresponding key
	 * @param string $key
	 * @param mixed $value
	 * @param array $array Target array in PHP
	 * @return boolean
	 */
	public function set($key, $value, &$array=false)
	{
		$this->lastkey = $key;
		if( $array === false )
		{
			$array = & $this->store;
		}
		$this->set_array($array, $this->group.$key, $value);
		$this->accesskey[$key] = md5($key);
		$this->keyaccess[md5($key)] = $key;
		if( preg_match("/passw/i", $key) ) $this->def($key, "ispassword", true);
		$this->def($key, "default", $value);
		if( isset($this->each[$this->group]) )
		{
			$this->def($this->each[$this->group][0], $this->each[$this->group][1]);
		}
		return true;
	}

	/**
	 * Set each key over all defined groups to the value
	 * @param string $key
	 * @param mixed $val
	 */
	public function seteach($key, $val=false)
	{
		$this->each[$this->group] = array($key, $val);
	}

	/**
	 * Load a struct layout from file and create an array from this file.
	 * Returns true when operation succeeded
	 * @param file $file
	 * @param array $array
	 * @return boolean
	 */
	public function preset_from_file($file, &$array=false)
	{
		if( !file_exists($file) ) return false;
		if( $array === false )
		{
			$array = & $this->store;
		}

		$stream = file_get_contents($file);
		$filecacheID = md5($stream);

		if( $this->usecache && file_exists("cache/_struct_cache_".$filecacheID) )
		{
			$cachedata = unserialize(gzuncompress(file_get_contents("cache/_struct_cache_".$filecacheID)));
			$this->store = $cachedata["store"];
			$this->meta = $cachedata["meta"];
			$this->accesskey = $cachedata["accesskey"];
			$this->keyaccess = $cachedata["keyaccess"];
			return true;
		}

		$rx = '#//.*?(\r\n|\n|\Z)+#';
		$stream = preg_replace($rx, "\$1", $stream);
		$rx = '#/\*.*?\*/#is';
		$stream = preg_replace($rx, "", $stream);
		$rx = '/; *(?:\r\n *|\n *|\Z)+/';
		$lines = preg_split($rx, $stream);
		if( $lines )
		{
			foreach( $lines as $line )
			{
				$line = $line.";";
				$line = ltrim($line);
				if( trim($line) )
				{
					$S = " \\t\\r\\n"; //Whitespaces
					$S = "\\s"; //Whitespaces
					$C = "//|SET|GROUP|EACH"; //Command
					$K = "[@a-zA-Z0-9_#+~.]"; //Key
					$L = "(?:->|=>)"; //Leveldelimiter
					$A = "(?::|=)"; //Assignmentdelimiter
					$E = ";"; //Endline command
					//[ \t\r\n]*(//|SET|GROUP|EACH)?[ \t\r\n]*((?:(?:[a-zA-Z0-9_#+~.]|(?:=>|->))(?:(?:->|=>)|(?::|=))*?)*)(?::|=)?((?:\\;|[^;])*?);
					$rx = "[".$S."]*(".$C.")?[".$S."]*((?:(?:".$K."|(?:=>|->|-))(?:".$L."|".$A.")*?)*)".$A."?((?:\\\\".$E."|[^".$E."])*?);";
					if( substr(ltrim($line), 0, 2) == "//" || substr(ltrim($line), 0, 1) == "#" )
					{
						//This line is a comment and gets no attention
					}
					elseif( preg_match("!".$rx."!", ltrim($line), $match) )
					{
						//print_r($match);
						switch ($match[1])
						{
							case "GROUP":
								$this->group($match[2]);
								break;
							case "EACH":
								$this->seteach($match[2], $match[3]);
								break;
							case "SET":
								$this->set($match[2], $match[3], $array);
								break;
							case "":
								//$split = preg_split("/".$A."/", $match[2], 2);
								$key = trim($match[2]);
								$val = trim($match[3]);
								$this->def($key, $val);
								break;
						}
					}
					else
					{
						throw new Exception("Something went wrong!");
					}
				}
			}
		}
		if( $this->usecache )
		{
			$cachedata = array("store" => $this->store, "meta" => $this->meta, "accesskey" => $this->accesskey, "keyaccess" => $this->keyaccess);
			file_put_contents("cache/_struct_cache_".$filecacheID, gzcompress(serialize($cachedata), 9));
		}
		return true;
	}

	/**
	 * Set a session
	 * @param string $id
	 */
	public function session($id)
	{
		$this->sesh = md5($id);
	}

	/**
	 * Define the creator library for struct output. This must be an valid instanciated object in a special named form. Please refer to the manual for this method.
	 * @param object $obj
	 */
	public function creator(&$obj)
	{
		$this->creator[] = $obj;
	}

	/**
	 * Append a mixed value to a key
	 * @param array $array
	 * @param string $key
	 * @param mixed $data
	 */
	private function set_array(&$array, $key, $data)
	{
		$rx = "/^((?:[^=>]|[^->])*?)(?:=>|->|\Z)(.*)/";
		preg_match($rx, $key, $match);

		$this->accesskey[$key] = md5($key);
		$this->keyaccess[md5($key)] = $key;

		if( !$match[2] )
		{
			if( !trim($match[1]) )
			{
				$array[] = $data;
			}
			else
			{
				$array[$match[1]] = $data;
			}
			return true;
		}
		if( !trim($match[1]) ) $this->set_array($array[], $match[2], $data);
		else $this->set_array($array[$match[1]], $match[2], $data);
	}

	/**
	 * This method is an alias for get_meta
	 * @param string $key
	 * @return array
	 */
	public function get_def($key)
	{
		return $this->get($key);
	}

	/**
	 * Get meta data to associated key
	 * @param string $key
	 * @return mixed
	 */
	public function get($key=null)
	{
		return (isset($key) ? $this->getkey($key, $this->meta) : $this->meta);
	}

	public function has($key=null)
	{
		if( isset($key) && isset($this->accesskey[$key]) )
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	public function getmeta($key=null)
	{
		return $this->get($key);
	}

	/**
	 * Get a key from the struct. If array is not given, it will try to get the desired data from current active array. If key is not defined, it returns the whole array.
	 * @param string $key
	 * @param array $array
	 * @return mixed
	 */
	public function getkey($key=null, $array=false)
	{
		if( $array === false )
		{
			$array = & $this->store;
		}

		if( !isset($key) ) return $array;

		//print_r($this->keyaccess);

		if( isset($this->keyaccess[$key]) ) $key = $$this->keyaccess[$key];

		preg_match("/^((?:[^=>]|[^->])*?)(?:=>|->|\Z)(.*)/", $key, $match);
		$nextkey = $this->no_case_decision($array, $match[1]);

		//print_r($nextkey);

		if( trim($match[2]) )
		{
			if( isset($array[$nextkey]) )
			{
				return $this->getkey($match[2], $array[$nextkey]);
			}
		}
		else
		{
			return $array[$nextkey];
		}
	}

	/**
	 * Helper, tries to determine the given key regardless of the given case
	 * @param array $stack
	 * @param string $needle
	 * @return mixed or raises an error.
	 */
	private static function no_case_decision($stack, $needle)
	{
		if( isset($stack[$needle]) ) return $needle;
		if( !is_array($stack) ) return $needle;
		$list = array_keys($stack);
		if( $list )
		{
			foreach( $list as $keyname )
			{
				if( strtolower($keyname) == strtolower($needle) )
				{
					$suggest[] = $keyname;
				}
			}
			if( count($suggest) >= 2 )
			{
				trigger_error('You tried to access "'.$needle.'" but there are only "'.implode('", "', $suggest).'" [CASE DECISION PROBLEM]', E_USER_WARNING);
				return $this->suggestion_decision_maker($needle, $suggest);
				return $needle;
			}
			elseif( count($suggest) == 1 )
			{
				return current($suggest);
			}
			else
			{
				trigger_error('No value to access by "'.$needle.'"', E_USER_WARNING);
				return $needle;
			}
		}
	}

	private function suggestion_decision_maker($needle, $suggestionlist)
	{
		foreach( $suggestionlist as $suggested )
		{
			$checklist[$suggested] = similar_text($needle, $suggested);
		}
		arsort($checklist);
		reset($checklist);
		return key($checklist);
	}

	/**
	 * Includes style stream from css file into html
	 * @param templatename $skin
	 */
	private function _css($skin=false)
	{
		$defaultskin = dirname(__FILE__)."/".__CLASS__."/"."default"."/"."style.css";
		$userskin = dirname(__FILE__)."/".__CLASS__."/".$skin."/"."style.css";
		if( !$skin && file_exists($defaultskin) )
		{
			$style = file_get_contents($defaultskin);
		}
		elseif( $skin && file_exists($userskin) )
		{
			$style = file_get_contents($userskin);
		}
		if( $style )
		{
			$this->h("<style>");
			echo str_replace("%path%", "classes/".__CLASS__, $style);
			$this->h("</style>");
		}
	}

	/**
	 * Includes JS-Functions in html
	 */
	private function _js()
	{
		$js = <<<EOF

EOF;
		$this->h("<script>");
		echo $js;
		$this->h("</script>");
	}

	/**
	 * This method creats the whole html code for an given struct or key
	 * @param array $array
	 * @param string $key
	 * @param boolean $return
	 * @return string
	 */
	public static function draw($array=false, $key=false, $return=false)
	{
		//			echo "ISARRAY:".is_array($array).": ";
		//			echo "ARRAY:".$array.": ";
		//			echo "KEY:".$key.": ";
		//			echo "RETURN:".$return.": ";

		if( !is_array($array) && $array === false && $key === false && $return === false )
		{
			$array = $this->store;
		}
		elseif( !is_array($array) && $array !== false && $key === false && $return === false )
		{
			$return = $key;
			$key = $array;
			$array = $this->store;
		}
		elseif( !is_array($array) && $array !== false && $key !== false && $return === false )
		{
			$return = $key;
			$key = $array;
			$array = $this->store;
		}

		//			echo "ISARRAY:".is_array($array).": ";
		//			echo "ARRAY:".$array.": ";
		//			echo "KEY:".$key.": ";
		//			echo "RETURN:".$return.": ";
		//			exit;

		$buildin = new buildincontrols;
		$this->creator($buildin);
		if( $return ) ob_start();
		$this->_css("default");
		$this->_js();
		$this->h('<div class="struct_root">');
		if( $key )
		{
			$array = $this->getkey($key, $array);
			$this->htmlarray($array, $key);
		}
		else
		{
			$this->htmlarray($array);
		}
		$this->h('</div>');
		if( $return ) return ob_get_clean();
	}

	/**
	 * Creates a html structure for an array
	 * @param mixed $node
	 * @param string $currentkey
	 * @param boolean $inithidden
	 */
	private function htmlarray($node, $currentkey="", $inithidden=false)
	{
		$this->h('<div class="listelement '.($inithidden ? "init_hidden" : "").'">');
		$this->h('<ul class="struct">');

		if( $node ) foreach( $node as $key => $data )
			{
				if( is_array($data) )
				{
					$expand = ($_COOKIE[md5($this->sesh.$currentkey."->".$key)] ? "" : "is_expanded");
					$inithidden = ($_COOKIE[md5($this->sesh.$currentkey."->".$key)] ? true : false);
					$this->h('<li class="node">');
					$this->h('<div class="element can_expand '.$expand.'" id="'.md5($this->sesh.$currentkey."->".$key).'" onclick="struct.toggle(this);" onmouseover="struct.over(this);" onmouseout="struct.out(this);">');
					$this->h('<a class="name">');
					$this->h($key);
					$this->h('</a>');
					$this->h('</div>');
					$this->htmlarray($data, (!$currentkey ? $key : $currentkey."->".$key), $inithidden);
					$this->h("</li>");
				}
				else
				{
					$this->h('<li class="node">');
					$this->h('<div class="element" onmouseover="struct.over(this);" onmouseout="struct.out(this);">');
					$this->h($this->build_node(($currentkey ? $currentkey."->" : "").$key, $data, $key));
					$this->h('</div>');
					$this->h('</li>');
				}
			}
		$this->h("</ul>");
		$this->h('</div>');
		return;
	}

	/**
	 * Creates a node for the given key
	 * @param string $key
	 * @param mixed $data
	 * @param string $name
	 */
	private function build_node($key, $data, $name=null)
	{
		$node = new node;
		$node->command("input");
		$node->setProperty("name", md5($key));
		$node->setProperty("readonly", "readonly");
		$this->order++;
		$node->setProperty("tabindex", $this->order);

		$set = $this->get($key, $this->meta);
		if( !$this->creator )
		{
			die("Cant find an element builder");
		}

		foreach( $this->creator as $cid => $cobj )
		{
			if( method_exists($cobj, "construct") )
			{
				$cobj->name = $name;
				$cobj->key = md5($key);
				$cobj->default = $set["default"];
				$cobj->param = "NA in construct";
				$cobj->value = &$data;
				$cobj->obj = &$node;
				call_user_func(array($cobj, "construct"));
			}
		}

		if( $set )
		{
			foreach( $set as $fn => $fv )
			{
				$fc = false;
				foreach( $this->creator as $cid => $cobj )
				{
					if( method_exists($cobj, $fn) && !$fc )
					{
						$cobj->name = $name;
						$cobj->key = md5($key);
						$cobj->default = $set["default"];
						$cobj->param = $fv;
						$cobj->value = &$data;
						$cobj->obj = &$node;
						call_user_func(array($cobj, $fn));
						$fc = true;
					}
				}
			}
		}

		$node->setProperty("value", $data);
		$this->h($node->get());
	}

	/**
	 * Swiss army knife for merging to structs or array into one.
	 * @param array $reference
	 * @param array $array_to_be_set
	 * @param boolean $just_alter_existing_values
	 * @param boolean $truncate_non_referenced
	 * @return boolean
	 */
	public function array_adapt($reference, &$array_to_be_set=false, $just_alter_existing_values=null, $truncate_non_referenced=null)
	{
		if( isset($reference) && !is_array($array_to_be_set) )
		{
			$array_to_be_set = & $this->$store;
		}

		if( $reference ) foreach( $reference as $key => $value )
			{
				if( !$just_alter_existing_values && is_array($reference[$key]) )
				{
					$this->array_adapt($reference[$key], $array_to_be_set[$key], $just_alter_existing_values, $truncate_non_referenced);
				}
				elseif( $just_alter_existing_values && is_array($reference[$key]) && is_array($array_to_be_set[$key]) )
				{
					$this->array_adapt($reference[$key], $array_to_be_set[$key], $just_alter_existing_values, $truncate_non_referenced);
				}
				elseif( !$just_alter_existing_values && !isset($array_to_be_set[$key]) )
				{
					$array_to_be_set[$key] = $value;
				}
				elseif( $just_alter_existing_values && isset($array_to_be_set[$key]) )
				{
					$array_to_be_set[$key] = $value;
				}
			}
		if( $truncate_non_referenced && $array_to_be_set ) foreach( $array_to_be_set as $key => $value )
			{
				if( $truncate_non_referenced && !isset($reference[$key]) )
				{
					unset($array_to_be_set[$key]);
				}
			}
		return true;
	}

	/**
	 * Kind of output buffer for single line html commands
	 * @param string $html
	 */
	private static function h($html)
	{
		echo $html.(substr($html, -1, 1) == ">" ? "" : "");
	}

}

?>
